package org.apache.lucene.store;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.IOException;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;

/**
 * File-based {@link Directory} implementation that uses mmap for input.
 * 
 * <p>
 * To use this, invoke Java with the System property
 * org.apache.lucene.FSDirectory.class set to
 * org.apache.lucene.store.MMapDirectory. This will cause {@link
 * FSDirectory#getDirectory(File,boolean)} to return instances of this class.
 */
public class MMapDirectory extends FSDirectory {

	private static class MMapIndexInput extends IndexInput {

		private ByteBuffer buffer;
		private final long length;

		private MMapIndexInput(RandomAccessFile raf) throws IOException {
			this.length = raf.length();
			this.buffer = raf.getChannel().map(MapMode.READ_ONLY, 0, length);
		}

		public byte readByte() throws IOException {
			return buffer.get();
		}

		public void readBytes(byte[] b, int offset, int len) throws IOException {
			buffer.get(b, offset, len);
		}

		public long getFilePointer() {
			return buffer.position();
		}

		public void seek(long pos) throws IOException {
			buffer.position((int) pos);
		}

		public long length() {
			return length;
		}

		public Object clone() {
			MMapIndexInput clone = (MMapIndexInput) super.clone();
			clone.buffer = buffer.duplicate();
			return clone;
		}

		public void close() throws IOException {
		}
	}

	private static class MultiMMapIndexInput extends IndexInput {

		private ByteBuffer[] buffers;
		private int[] bufSizes; // keep here, ByteBuffer.size() method is
								// optional

		private final long length;

		private int curBufIndex;
		private final int maxBufSize;

		private ByteBuffer curBuf; // redundant for speed: buffers[curBufIndex]
		private int curAvail; // redundant for speed: (bufSizes[curBufIndex] -
								// curBuf.position())

		public MultiMMapIndexInput(RandomAccessFile raf, int maxBufSize)
				throws IOException {
			this.length = raf.length();
			this.maxBufSize = maxBufSize;

			if (maxBufSize <= 0)
				throw new IllegalArgumentException("Non positive maxBufSize: "
						+ maxBufSize);

			if ((length / maxBufSize) > Integer.MAX_VALUE)
				throw new IllegalArgumentException(
						"RandomAccessFile too big for maximum buffer size: "
								+ raf.toString());

			int nrBuffers = (int) (length / maxBufSize);
			if ((nrBuffers * maxBufSize) < length)
				nrBuffers++;

			this.buffers = new ByteBuffer[nrBuffers];
			this.bufSizes = new int[nrBuffers];

			long bufferStart = 0;
			FileChannel rafc = raf.getChannel();
			for (int bufNr = 0; bufNr < nrBuffers; bufNr++) {
				int bufSize = (length > (bufferStart + maxBufSize)) ? maxBufSize
						: (int) (length - bufferStart);
				this.buffers[bufNr] = rafc.map(MapMode.READ_ONLY, bufferStart,
						bufSize);
				this.bufSizes[bufNr] = bufSize;
				bufferStart += bufSize;
			}
			seek(0L);
		}

		public byte readByte() throws IOException {
			// Performance might be improved by reading ahead into an array of
			// eg. 128 bytes and readByte() from there.
			if (curAvail == 0) {
				curBufIndex++;
				curBuf = buffers[curBufIndex]; // index out of bounds when too
												// many bytes requested
				curBuf.position(0);
				curAvail = bufSizes[curBufIndex];
			}
			curAvail--;
			return curBuf.get();
		}

		public void readBytes(byte[] b, int offset, int len) throws IOException {
			while (len > curAvail) {
				curBuf.get(b, offset, curAvail);
				len -= curAvail;
				offset += curAvail;
				curBufIndex++;
				curBuf = buffers[curBufIndex]; // index out of bounds when too
												// many bytes requested
				curBuf.position(0);
				curAvail = bufSizes[curBufIndex];
			}
			curBuf.get(b, offset, len);
			curAvail -= len;
		}

		public long getFilePointer() {
			return (curBufIndex * (long) maxBufSize) + curBuf.position();
		}

		public void seek(long pos) throws IOException {
			curBufIndex = (int) (pos / maxBufSize);
			curBuf = buffers[curBufIndex];
			int bufOffset = (int) (pos - (curBufIndex * maxBufSize));
			curBuf.position(bufOffset);
			curAvail = bufSizes[curBufIndex] - bufOffset;
		}

		public long length() {
			return length;
		}

		public Object clone() {
			MultiMMapIndexInput clone = (MultiMMapIndexInput) super.clone();
			clone.buffers = new ByteBuffer[buffers.length];
			// No need to clone bufSizes.
			// Since most clones will use only one buffer, duplicate() could
			// also be
			// done lazy in clones, eg. when adapting curBuf.
			for (int bufNr = 0; bufNr < buffers.length; bufNr++) {
				clone.buffers[bufNr] = buffers[bufNr].duplicate();
			}
			try {
				clone.seek(getFilePointer());
			} catch (IOException ioe) {
				RuntimeException newException = new RuntimeException(ioe);
				newException.initCause(ioe);
				throw newException;
			}
			;
			return clone;
		}

		public void close() throws IOException {
		}
	}

	private final int MAX_BBUF = Integer.MAX_VALUE;

	public IndexInput openInput(String name) throws IOException {
		File f = new File(getFile(), name);
		RandomAccessFile raf = new RandomAccessFile(f, "r");
		try {
			return (raf.length() <= MAX_BBUF) ? (IndexInput) new MMapIndexInput(
					raf)
					: (IndexInput) new MultiMMapIndexInput(raf, MAX_BBUF);
		} finally {
			raf.close();
		}
	}

	public IndexInput openInput(String name, int bufferSize) throws IOException {
		return openInput(name);
	}
}
